今天我們來看看 call.respondText("Hello World!")
這段函數,是怎麼協助我們建立一個回應內容的。
我們先看到
然後,我們來看看 respondText()
的實作
public suspend fun ApplicationCall.respondText(
text: String,
contentType: ContentType? = null,
status: HttpStatusCode? = null,
configure: OutgoingContent.() -> Unit = {}
) {
val message = TextContent(text, defaultTextContentType(contentType), status).apply(configure)
respond(message)
}
TextContent
的實作如下
/**
* Represents a text content that could be sent
* @property text to be sent
*/
public class TextContent(
public val text: String,
override val contentType: ContentType,
override val status: HttpStatusCode? = null
) : OutgoingContent.ByteArrayContent() {
private val bytes = text.toByteArray(contentType.charset() ?: Charsets.UTF_8)
override val contentLength: Long
get() = bytes.size.toLong()
override fun bytes(): ByteArray = bytes
override fun toString(): String = "TextContent[$contentType] \"${text.take(30)}\""
}
這個類別繼承了 OutgoingContent.ByteArrayContent()
/**
* Variant of a [OutgoingContent] with payload represented as [ByteArray]
*/
public abstract class ByteArrayContent : OutgoingContent() {
/**
* Provides [ByteArray] which engine will send to peer
*/
public abstract fun bytes(): ByteArray
}
接著我們看到 OutgoingContent
,這個類別的內容非常多,
我們先根據其簽名和註解,推測其內容
/**
* Information about the content to be sent to the peer, recognized by a client or server engine
*/
public sealed class OutgoingContent {
根據註解來看,看起來是專門處理輸出資訊的類別。我們對這個類別的研究先停止在此,專心於 respondText
後續的段落。
接著,我們來看 defaultTextContentType(contentType)
/**
* Creates a default [ContentType] based on the given [contentType] and current call.
*
* If [contentType] is `null`, it tries to fetch an already set "Content-Type" response header.
* If the header is not available, `text/plain` is used. If [contentType] is specified, it uses it.
*
* Additionally, if a content type is `Text` and a charset is not set for a content type,
* it appends `; charset=UTF-8` to the content type.
*/
public fun ApplicationCall.defaultTextContentType(contentType: ContentType?): ContentType {
val result = when (contentType) {
null -> {
val headersContentType = response.headers[HttpHeaders.ContentType]
headersContentType?.let {
try {
ContentType.parse(headersContentType)
} catch (_: BadContentTypeFormatException) {
null
}
} ?: ContentType.Text.Plain
}
else -> contentType
}
return if (result.charset() == null && result.match(ContentType.Text.Any)) {
result.withCharset(Charsets.UTF_8)
} else {
result
}
}
這一段邏輯相對小複雜,幸好我們有註解可以協助說明這一段的目的。
If [contentType]
is null
, it tries to fetch an already set "Content-Type" response header.:
val result = when (contentType) {
null -> {
val headersContentType = response.headers[HttpHeaders.ContentType]
headersContentType
是一個 String?
,所以我們要試著將 headersContentType
parse 成 ContentType
類別
headersContentType?.let {
try {
ContentType.parse(headersContentType)
} catch (_: BadContentTypeFormatException) {
null
}
}
這邊的 ?.let
是一個 Kotlin 的常用小組合:如果前面的輸入不為 null
,就讓(let)輸入做 let
裡面所定義的事情,不然就往下進行輸入為 null
該做的事情。
這邊的 let 裡面試著做 parsing,如果失敗了,我們忽略收到的例外,視為 header is not available
If the header is not available, text/plain
is used.
} ?: ContentType.Text.Plain
如果有定義的話:If contentType is specified, it uses it.
else -> contentType
Additionally, if a content type is Text
and a charset is not set for a content type, it appends ; charset=UTF-8
to the content type.
return if (result.charset() == null && result.match(ContentType.Text.Any)) {
result.withCharset(Charsets.UTF_8)
} else {
result
}
到這邊,ApplicationCall.defaultTextContentType()
的行為完成了。
接著看到輸入的 status
,這是一個 HttpStatusCode
public data class HttpStatusCode(val value: Int, val description: String) : Comparable<HttpStatusCode> {
只要是網頁後端框架,基本上一定會看到 HttpStatusCode 這個物件。
雖然這段程式碼很多,不過裡面目前沒有特別吸引我們的邏輯,我們先略過不提。
我們往下繼續看 val message = TextContent(text, defaultTextContentType(contentType), status).apply(configure)
可以看到跟之前一樣的做法,用 .apply(configure)
加上新的行為。
看到這邊,我們知道了 respondText
是怎麼將輸入的字串轉換成一個 OutgoingContent
物件,並加上對應的 contentType
和 status
,以及看到了 HttpStatusCode
這個只要是網頁後端框架,基本上可以說必定存在的物件。
明天我們來看看 respond(message)
這個函數,是怎麼將訊息回傳出去的。